package weeny.pre.face.treetable;


import static weeny.pre.face.ColorTextField.COLOR_FLD_PATTERN;
import static weeny.pre.io.parser.EnumParser.SHAPE_PARSER;

import java.awt.BorderLayout;
import java.awt.Color;
import java.awt.Component;
import java.awt.Cursor;
import java.awt.Dimension;
import java.awt.Graphics;
import java.awt.Insets;
import java.awt.datatransfer.DataFlavor;
import java.awt.datatransfer.StringSelection;
import java.awt.datatransfer.Transferable;
import java.awt.dnd.DnDConstants;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.awt.event.MouseAdapter;
import java.awt.event.MouseEvent;
import java.util.ArrayList;
import java.util.EventObject;
import java.util.List;

import javax.swing.AbstractCellEditor;
import javax.swing.Box;
import javax.swing.DropMode;
import javax.swing.Icon;
import javax.swing.ImageIcon;
import javax.swing.JButton;
import javax.swing.JCheckBox;
import javax.swing.JComponent;
import javax.swing.JLabel;
import javax.swing.JOptionPane;
import javax.swing.JPanel;
import javax.swing.JPopupMenu;
import javax.swing.JScrollPane;
import javax.swing.JTable;
import javax.swing.JToggleButton;
import javax.swing.JTree;
import javax.swing.SwingUtilities;
import javax.swing.TransferHandler;
import javax.swing.border.BevelBorder;
import javax.swing.border.TitledBorder;
import javax.swing.table.TableCellEditor;
import javax.swing.table.TableCellRenderer;
import javax.swing.tree.TreePath;

import prefuse.data.Table;
import prefuse.data.expression.Predicate;
import prefuse.data.parser.IntArrayParser;
import prefuse.util.io.IOLib;
import prefuse.visual.VisualItem;
import weeny.pre.face.FilterBox;
import weeny.pre.face.PreJTable;
import weeny.pre.io.parser.ColorParser;
import weeny.util.io.ModUtil;
import weeny.util.logs.Debug;


/**
 * To use this class, please write your own {@link AbstractPreModel}, and pass
 * it into the constructor, and it is done. Of course to use your model, you may
 * need to create your own {@link AbstractTreeNode}, which takes care of the
 * behavior of rendering, editing, and something else. You can find two examples
 * in {@link PreTreeModel} and {@link PreGroupModel}
 * 
 * There are two options you can pick:
 * 1) {@link #enableDnD(boolean)} allows you to select multiple rows, drag them,
 *    and drop it on another row. To use this DnD function, please define a
 *    proper action in {@link AbstractTreeNode#dropNodes(java.util.Collection)}.
 * 2) {@link #enableColumnFilter(boolean)} allows you to right click the table
 *    header to dynamically choose what columns to see.
 *    
 * @author weeny
 *
 */
public class PreJTreeTable extends JTreeTable{
	private static Icon ROW_INSERT_ICON = new ImageIcon(
			IOLib.urlFromString("but_row_insert.png", JTreeTable.class, false));
	private static Icon FOCUS_ADD_ICON = new ImageIcon(
			IOLib.urlFromString("but_focus_add.png",  JTreeTable.class, false));
	private static Icon DRAG_ON_ICON = new ImageIcon(
			IOLib.urlFromString("but_drag_on.png",    JTreeTable.class, false));
	private static Icon DRAG_OFF_ICON = new ImageIcon(
			IOLib.urlFromString("but_drag_off.png",   JTreeTable.class, false));
	private static Icon SHOW_ALL_ICON = new ImageIcon(
			IOLib.urlFromString("but_show_all.png",   JTreeTable.class, false));
	private static Icon SHOW_FEW_ICON = new ImageIcon(
			IOLib.urlFromString("but_show_few.png",   JTreeTable.class, false));
	private static Icon SHOW_NUM_ICON = new ImageIcon(
			IOLib.urlFromString("but_show_num.png",   JTreeTable.class, false));
	private static Icon SHOW_NODE_ICON = new ImageIcon(
			IOLib.urlFromString("but_show_node.png",  JTreeTable.class, false));
	private static Icon REMOVE_ICON = new ImageIcon(
			IOLib.urlFromString("but_remove.png",     JTreeTable.class, false));
	
	protected AbstractPreModel preModel = null;
	public PreJTreeTable(AbstractPreModel preModel) {
		super();
		resetTreeTable(preModel, true);
		enableColumnFilter(true);		
		setUI(new PreGroupTreeTableUI());
	}

	protected TableCellRenderer treeRenderer = null;
	protected TreeTableCellEditor treeEditor = null;
	
	@Override
	public TableCellEditor getCellEditor(int row, int column) {
		int modelCol = convertColumnIndexToModel(column);
		if (modelCol == 0) {
			return treeEditor;
		}
		return preModel.selectEditor(nodeForRow(row), modelCol);
	}
	@Override
	public TableCellRenderer getCellRenderer(int row, int column) {
		int modelCol = convertColumnIndexToModel(column);
		if (modelCol == 0) {
			return treeRenderer;
		}
		return preModel.selectRenderer(nodeForRow(row), modelCol);
	}
	
	protected MouseAdapter columnFilterAdapter = new MouseAdapter() {
		@Override
		public void mouseClicked(MouseEvent e) {
			if (e.getButton() == 3) {
				resetColumnFilterPopup();
				colFilterPopup.show(PreJTreeTable.this, e.getX(), e.getY());
			}
		}
	};
	protected JPopupMenu colFilterPopup = new JPopupMenu();
	protected void resetColumnFilterPopup(){
		colFilterPopup.removeAll();
		//init the column selector popup menu
		List<String> list = preModel.getColumnNameCandidates();
		for (String column : list) {
			boolean on = false;
			for(int i = 0; i<getColumnCount(); i++){
				if (convertColumnIndexToModel(i) == 0){
					continue;
				}
				if (getColumnName(i).equals(column)) {
					on = true;
					break;
				}
			}
			JCheckBox check = new JCheckBox(column, on);
			check.addActionListener(columnFilterListener);
			colFilterPopup.add(check);
		}
	}
	ActionListener columnFilterListener = new ActionListener() {
		//listen to columnFilterPopup, change displayed columns if necessary
		public void actionPerformed(ActionEvent e) {
			String column = ((JCheckBox)e.getSource()).getText();
			boolean on = ((JCheckBox)e.getSource()).isSelected();
			preModel.updateColumn(column, on);
			resetTreeTable(preModel, false);
			updateUI();
			setUI(new PreGroupTreeTableUI());
		}
	};
	
	public void removeSelectedNodes(boolean altDown){
		int[] selected = getSelectedRows();
		if (selected == null || selected.length == 0){
			Debug.info("No tuples are selected");
			return;
		}
		List<AbstractTreeNode> list = new ArrayList<AbstractTreeNode>();
		for (int row : selected){
			list.add(nodeForRow(row));
		}
		boolean modified = false;
		for (AbstractTreeNode node : list) {
			boolean flag = node.remove(altDown); 
			if (flag) {
				modified = flag;
			}
		}
		if (modified) {
			updateUI();
		}
	}
	/**
	 * By enabling DnD, you can select multiple tuples and drag them on a focus
	 * group to add them.
	 */
	public void enableDnD(boolean flag){
		if (!flag) {
			setDragEnabled(false);
			setTransferHandler(null);
			return;
		}
		setDragEnabled(true);
		setDropMode(DropMode.ON_OR_INSERT_ROWS);
		setTransferHandler(new TransferHandler(){
			@Override
			public int getSourceActions(JComponent c) {
				return DnDConstants.ACTION_COPY;
			}
			@Override
			protected Transferable createTransferable(JComponent c) {
				JTable table = (JTable) c;
				int[] row = table.getSelectedRows();
				//we encode all the rows into a string by using int array parser
				return new StringSelection(new IntArrayParser().format(row));
			}
			@Override
			public boolean canImport(TransferSupport support) {
				return support.isDataFlavorSupported(DataFlavor.stringFlavor);
			}
			@Override
			public boolean importData(TransferSupport support) {
				if (!support.isDrop() || !canImport(support)) {
					return false;
				}
				PreJTreeTable table = (PreJTreeTable) support.getComponent();
				JTable.DropLocation dropLocation = 
					(JTable.DropLocation) support.getDropLocation();
				if (dropLocation.isInsertRow()) {
					Debug.prob("Ignore the insertion drop!");
					return false;
				}
				int row = dropLocation.getRow();
				int[] fromRows = null;
				try {
					fromRows = (int[]) new IntArrayParser().parse(
							support.getTransferable().getTransferData(
									DataFlavor.stringFlavor).toString());
				} catch (Exception e) {
					return false;
				}
				AbstractTreeNode tn = table.nodeForRow(row);
				List<AbstractTreeNode> list = new ArrayList<AbstractTreeNode>();
				for(int rowIndex : fromRows){
					list.add(table.nodeForRow(rowIndex));
				}
				boolean result = tn.dropNodes(list);
				updateUI();
				return result;
			}
		});
	}
	
	public void enableColumnFilter(boolean flag){
		getTableHeader().removeMouseListener(columnFilterAdapter);
		if (flag) {
			getTableHeader().addMouseListener(columnFilterAdapter);
		}
	}
	public void setFilter(Predicate filter){
		preModel.setFilter(filter);
	}
	public void setFilter(final FilterBox filter){
		if (filter == null) {
			preModel.setFilter(null);
			return;
		}
		filter.addActionListener(new ActionListener() {
			public void actionPerformed(ActionEvent e) {
				Debug.info("Filter is changed to: "+filter.getExpr());
				setFilter(filter.getPred());
			}
		});
	}
	public JComponent wrap(Dimension dimension){
		JLabel label = (JLabel) getTableHeader().getDefaultRenderer();		
		label.setPreferredSize(new Dimension(0, 15));
		JScrollPane scrollPane = new JScrollPane(this);
		final TitledBorder boxBorder = 
			new TitledBorder("Item groups (double click to expand)"){
			public void paintBorder(Component c, Graphics g, int x, int y,
					int width, int height) {
				Color color = g.getColor();
				g.setColor(PreJTable.LABEL_GRAY);
				g.fillRect(x, y, width, height);
				super.paintBorder(c, g, x, y, width, height);
				g.setColor(color);
			}
		};
		scrollPane.setPreferredSize(dimension);
		JPanel panel = new JPanel(new BorderLayout());
		panel.add(scrollPane, BorderLayout.CENTER);
		scrollPane.getViewport().setBackground(Color.white);
		scrollPane.getViewport().setOpaque(true);
		
		final JToggleButton dndTog = 
			new SimpleToggleButton(DRAG_OFF_ICON, DRAG_ON_ICON);
//		JCheckBox dndCheck = new JCheckBox("drag&drop");
		dndTog.addActionListener(new ActionListener() {
			public void actionPerformed(ActionEvent e) {
				enableDnD(dndTog.isSelected());
			}
		});
		panel.add(scrollPane, BorderLayout.CENTER);
		
		JButton rowInsertButton = new SimpleButton(ROW_INSERT_ICON);
		rowInsertButton.setToolTipText("Add a new tuple in the selected table");
		rowInsertButton.addActionListener(new ActionListener() {
			public void actionPerformed(ActionEvent e) {
				int[] selected = getSelectedRows();
				if (selected == null || selected.length != 1) {
					Debug.prob("Please select a single row and try again");
					return;
				}
				AbstractTreeNode node = nodeForRow(selected[0]);
				if (node.addChild(ModUtil.isAltDown(e))) {
					updateUI();
				}
			}
		});
		JButton removeButton = new SimpleButton(REMOVE_ICON);
		removeButton.addActionListener(new ActionListener() {
			public void actionPerformed(ActionEvent e) {
				removeSelectedNodes(ModUtil.isAltDown(e));
			}
		});
		Box controlBox = Box.createHorizontalBox();
		controlBox.add(Box.createHorizontalGlue());
		
		JButton addFocusButton = new SimpleButton(FOCUS_ADD_ICON);
		addFocusButton.addActionListener(new ActionListener() {
			public void actionPerformed(ActionEvent e) {
				if (preModel.getVisualization() == null) {
					Debug.prob("Need a visualization to continue, quit.");
					return;
				}
				String group = JOptionPane.showInputDialog("New group name:");
				if (group == null) {
					Debug.info("Creating group operation is cancelled");
					return;
				}
				if (preModel.getVisualization().getGroup(group)!=null) {
					JOptionPane.showConfirmDialog(
							null, "The group already exists");
					return;
				}
				preModel.getVisualization().addFocusGroup(group);
				Debug.info(group+" is added as a focus group");
				updateUI();
			}
		});
		controlBox.add(addFocusButton);
		controlBox.add(rowInsertButton);
		controlBox.add(Box.createHorizontalStrut(10));
		controlBox.add(removeButton);
		panel.add(controlBox, BorderLayout.NORTH);
		panel.setBorder(boxBorder);
		
		
		Box showCheckBox = Box.createHorizontalBox();
		showCheckBox.add(Box.createHorizontalGlue());
		
		final JToggleButton showAllFieldTog = 
			new SimpleToggleButton(SHOW_FEW_ICON, SHOW_ALL_ICON);
		showAllFieldTog.setSelected(PreGroupModel.SHOW_ALL_FIELDS);
		showAllFieldTog.setToolTipText("show selected or detailed columns");
		showAllFieldTog.addActionListener(new ActionListener() {
			public void actionPerformed(ActionEvent e) {
				PreGroupModel.SHOW_ALL_FIELDS = showAllFieldTog.isSelected();
				updateUI();
			}
		});
		final JToggleButton showEndPointTog = 
			new SimpleToggleButton(SHOW_NUM_ICON, SHOW_NODE_ICON);
		showEndPointTog.setSelected(PreGroupModel.SHOW_SOURCE_TARGET);
		showEndPointTog.setToolTipText("show source/target as node or number");
		showEndPointTog.addActionListener(new ActionListener() {
			public void actionPerformed(ActionEvent e) {
				PreGroupModel.SHOW_SOURCE_TARGET = showEndPointTog.isSelected();
				updateUI();
			}
		});
		showCheckBox.add(dndTog);
		showCheckBox.add(Box.createHorizontalStrut(2));
		showCheckBox.add(showEndPointTog);
		showCheckBox.add(Box.createHorizontalStrut(2));
		showCheckBox.add(showAllFieldTog);
		panel.add(showCheckBox, BorderLayout.SOUTH);
		return panel;
	}
	protected AbstractTreeNode nodeForRow(int row){
		row = convertRowIndexToModel(row);
		//I think here we should use the row in the model
        TreePath treePath = tree.getPathForRow(row);
        return (AbstractTreeNode) treePath.getLastPathComponent();         
	}
	
	protected void resetTreeTable(AbstractPreModel model, boolean updateTree){
		this.preModel = model;
		if (updateTree) {
			tree = new TreeTableCellRenderer(model, this);
		}
		tree.setRootVisible(false);
		model.setTable(this, tree);
		tree.setCellRenderer(model);
		setModel(new TreeTableModelAdapter(model, tree));
		JTreeTableSelectionModel selModel = new JTreeTableSelectionModel(tree);
		tree.setSelectionModel(selModel);
		setSelectionModel(selModel.getListSelectionModel());

		treeRenderer = (TableCellRenderer) tree;
		treeEditor = new TreeTableCellEditor(tree, this);

		ColorParser colorParser = new ColorParser();
		for(Table preTable : model.getTableList()){
			for(int i = 0; i<preTable.getColumnCount(); i++){
				String field = preTable.getColumnName(i);
				if (field.equals(VisualItem.SHAPE)) {
					preTable.getColumn(i).setParser(SHAPE_PARSER);
				}
				if (COLOR_FLD_PATTERN.matcher(field).matches()){
					preTable.getColumn(i).setParser(colorParser);
				}
			}
		}
		
		setShowGrid(false);
		
		setIntercellSpacing(new Dimension(0, 0));
		setRowHeight(20);		
	}
}


class TreeTableCellEditor extends AbstractCellEditor implements TableCellEditor{
	protected JTree tree = null;
	protected JTreeTable table = null;
	public TreeTableCellEditor(JTree tree, JTreeTable jTreeTable){
		this.tree = tree;
		this.table = jTreeTable;
	}
	public Component getTableCellEditorComponent(JTable table,
			Object value, boolean isSelected, int r, int c) {
		return tree;
	}
	@Override
	public boolean shouldSelectCell(EventObject anEvent) {
		return false;
	}
	@Override
	public boolean isCellEditable(EventObject eo) {
		if (eo instanceof MouseEvent) {
            MouseEvent me = (MouseEvent)eo;
            int counter = table.convertColumnIndexToView(0);
            MouseEvent nme = new MouseEvent(tree, me.getID(),
                       me.getWhen(), me.getModifiers(),
                       me.getX() - table.getCellRect(0, counter, true).x,
                       me.getY(), me.getClickCount(),
                       me.isPopupTrigger());
//			tree.dispatchEvent(nme);
			int row = table.rowAtPoint(me.getPoint());
			if (((PreJTreeTable) table).nodeForRow(row).nodeClicked(nme)){
				//if the click event is used
				SwingUtilities.invokeLater(new Runnable() {
					public void run() {
						table.updateUI();
					}
				});
			}
			else if (me.getClickCount() == 2) {
    			if (tree.isCollapsed(row)) {
    				tree.expandRow(row);
    			}
    			else if (tree.isExpanded(row)){
    				tree.collapseRow(row);
    			}
			}
		}
		return false;
	}
	public Object getCellEditorValue() {
		return null;
	}
}

class SimpleButton extends JButton{
	private boolean hover = false;
	public SimpleButton(Icon icon) {
		this.setIcon(icon);
		this.setBorder(SimpleToggleButton.BORDER);
		this.setFocusable(false);
		this.setCursor(Cursor.getPredefinedCursor(Cursor.HAND_CURSOR));
		this.setRolloverEnabled(true);
		this.addMouseListener(new MouseAdapter() {
			@Override
			public void mouseEntered(MouseEvent e) {
				hover = true;
			}
			@Override
			public void mouseExited(MouseEvent e) {
				hover = false;
			}
		});
	}
	protected void paintComponent(Graphics g) {
		if (hover && getModel().isPressed() && !isBorderPainted()){
			setBorderPainted(true);
		}
		else if ((!hover || !getModel().isPressed()) && isBorderPainted()){
			setBorderPainted(false);
		}
		super.paintComponent(g);
	}
	public JButton withListener(ActionListener listener){
		this.addActionListener(listener);
		return this;
	}
}
class SimpleToggleButton extends JToggleButton{
	static Insets INSETS = new Insets(4, 4, 4, 4);
	static BevelBorder BORDER = new BevelBorder(BevelBorder.LOWERED){
		@Override
		public Insets getBorderInsets(Component c) {
			return INSETS;
		}
	};
	@Override
	protected void paintComponent(Graphics g) {
		if (getModel().isSelected() && !isBorderPainted()) {
			setBorderPainted(true);
		}
		if (!getModel().isSelected() && isBorderPainted()) {
			setBorderPainted(false);
		}
		super.paintComponent(g);
	}
	public SimpleToggleButton(Icon dftIcon, Icon selectedIcon) {
		this.setBorder(BORDER);
		this.setIcon(dftIcon);
		this.setSelectedIcon(selectedIcon);
		this.setFocusable(false);
		this.setCursor(Cursor.getPredefinedCursor(Cursor.HAND_CURSOR));
	}
}